// ═══════════════════════════════════════════════════════ // 3-PLAYER LED RACING GAME - XIAO RP2040 // Track: 5m WS2812B Strip (300 LEDs) // // PLAYER 1 (RED): Forward D0, Booster D1 // PLAYER 2 (GREEN): Forward D2, Booster D3 // PLAYER 3 (BLUE): Forward D4, Booster D5 // ═══════════════════════════════════════════════════════ #include // ── LED Strip Configuration ───────────────────────────── #define LED_PIN D6 // Data pin to LED strip #define NUM_LEDS 300 // 5 meters × 60 LEDs/meter = 300 LEDs #define LED_TYPE WS2812B #define COLOR_ORDER GRB #define BRIGHTNESS 100 CRGB leds[NUM_LEDS]; // ── Player Button Pins ────────────────────────────────── // Player 1 - RED #define P1_FORWARD_PIN D0 // Player 2 - GREEN #define P2_FORWARD_PIN D1 // Player 3 - BLUE #define P3_FORWARD_PIN D2 // ── Game Variables ────────────────────────────────────── struct Player { int position; // Current LED position (0-299) CRGB color; // Player color int forwardPin; // Forward button pin String name; // Player name bool finished; // Has player finished? unsigned long finishTime; // Time when finished int boostsRemaining; // Number of boosts left // Long press detection bool buttonPressed; // Current button state unsigned long pressStartTime; // When button was pressed bool moveExecuted; // Has move been executed for this press }; Player players[3]; unsigned long gameStartTime = 0; bool gameStarted = false; bool gameOver = false; int finishOrder = 0; // Game settings #define NORMAL_SPEED 4 // LEDs to move per forward press #define BOOSTED_SPEED 25 // LEDs to move with booster active #define MAX_BOOSTS 2 // Maximum number of boosts per player #define LONG_PRESS_TIME 500 // Hold button for 500ms to activate boost #define DEBOUNCE_DELAY 50 // Debounce time in ms #define FINISH_LINE (NUM_LEDS - 1) // Position 299 #define TRAIL_LENGTH 4 // Length of LED trail behind player // ── Track Features with Speed Modifiers ───────────────── // Hill 1: LEDs 17-31 (split into up and down) #define HILL1_START 17 #define HILL1_PEAK 24 // Middle of hill (17+31)/2 = 24 #define HILL1_END 31 #define HILL1_UP_SPEED 0.5 // Slow uphill #define HILL1_DOWN_SPEED 2.0 // Fast downhill! // Hill 2: LEDs 33-54 (split into up and down) #define HILL2_START 33 #define HILL2_PEAK 43 // Middle of hill (33+54)/2 ≈ 43 #define HILL2_END 54 #define HILL2_UP_SPEED 0.5 // Slow uphill #define HILL2_DOWN_SPEED 2.0 // Fast downhill! // Horizontal Spiral: LEDs 127-183 (FAST - gravity assists!) #define SPIRAL1_START 127 #define SPIRAL1_END 183 #define SPIRAL1_SPEED 2.0 // Double speed in spiral! // Vertical Spiral: LEDs 223-273 (VERY FAST - falling down!) #define SPIRAL2_START 223 #define SPIRAL2_END 273 #define SPIRAL2_SPEED 3.0 // Triple speed in vertical spiral! // ════════════════════════════════════════════════════════ void setup() { Serial.begin(115200); delay(1000); // Initialize FastLED FastLED.addLeds(leds, NUM_LEDS); FastLED.setBrightness(BRIGHTNESS); FastLED.clear(); FastLED.show(); // Initialize Player 1 - RED players[0].position = 0; players[0].color = CRGB::Red; players[0].forwardPin = P1_FORWARD_PIN; players[0].name = "RED"; players[0].finished = false; players[0].boostsRemaining = MAX_BOOSTS; players[0].buttonPressed = false; players[0].pressStartTime = 0; players[0].moveExecuted = false; // Initialize Player 2 - GREEN players[1].position = 0; players[1].color = CRGB::Green; players[1].forwardPin = P2_FORWARD_PIN; players[1].name = "GREEN"; players[1].finished = false; players[1].boostsRemaining = MAX_BOOSTS; players[1].buttonPressed = false; players[1].pressStartTime = 0; players[1].moveExecuted = false; // Initialize Player 3 - BLUE players[2].position = 0; players[2].color = CRGB::Blue; players[2].forwardPin = P3_FORWARD_PIN; players[2].name = "BLUE"; players[2].finished = false; players[2].boostsRemaining = MAX_BOOSTS; players[2].buttonPressed = false; players[2].pressStartTime = 0; players[2].moveExecuted = false; // Setup button pins (INPUT_PULLUP = active LOW) for(int i = 0; i < 3; i++) { pinMode(players[i].forwardPin, INPUT_PULLUP); } // Welcome message Serial.println("╔═══════════════════════════════════════════════════╗"); Serial.println("║ LED VELOCITY RACE - SPIRAL DASH ║"); Serial.println("║ 3-Player Racing Game 🏁 ║"); Serial.println("╚═══════════════════════════════════════════════════╝"); Serial.println(" Track Length: 300 LEDs (5 meters)"); Serial.println("──────────────────────────────────────────────────────"); Serial.println(" PLAYER 1 (RED) - Button: D0"); Serial.println(" PLAYER 2 (GREEN) - Button: D1"); Serial.println(" PLAYER 3 (BLUE) - Button: D2"); Serial.println("──────────────────────────────────────────────────────"); Serial.println(" Quick Press: Move 1 LED forward"); Serial.println(" Long Press (hold 0.5s): BOOST 5 LEDs forward"); Serial.println(" Boosts available: " + String(MAX_BOOSTS) + " per player"); Serial.println(" Total boost distance: " + String(MAX_BOOSTS * BOOSTED_SPEED) + " LEDs"); Serial.println("──────────────────────────────────────────────────────"); Serial.println("✓ Press any button to start the race!"); Serial.println(); // Show starting line showStartingPositions(); } // ════════════════════════════════════════════════════════ void loop() { if(!gameStarted) { // Wait for any player to press forward to start for(int i = 0; i < 3; i++) { if(digitalRead(players[i].forwardPin) == LOW) { startGame(); break; } } return; } if(gameOver) { // Show victory animation victoryAnimation(); return; } // Read buttons and update player positions for(int i = 0; i < 3; i++) { if(!players[i].finished) { updatePlayer(i); } } // Update LED strip displayRace(); FastLED.show(); // Check if all players finished checkGameOver(); } // ════════════════════════════════════════════════════════ void startGame() { gameStarted = true; gameStartTime = millis(); Serial.println("🏁🏁🏁 RACE STARTED! 🏁🏁🏁"); Serial.println(); Serial.println("🎢 TRACK FEATURES:"); Serial.println(" ⛰️↗ Hill 1 UP (LEDs 17-24): SLOW climb! 0.5x"); Serial.println(" ⛰️↘ Hill 1 DOWN (LEDs 25-31): FAST descent! 2x"); Serial.println(" ⛰️↗ Hill 2 UP (LEDs 33-43): SLOW climb! 0.5x"); Serial.println(" ⛰️↘ Hill 2 DOWN (LEDs 44-54): FAST descent! 2x"); Serial.println(" 🌀 Horizontal Spiral (LEDs 127-183): FAST! 2x speed!"); Serial.println(" 🌪️ Vertical Spiral (LEDs 223-273): SUPER FAST! 3x speed!"); Serial.println(); // Countdown animation - flash entire track in white for(int i = 3; i > 0; i--) { // Light up entire track in white fill_solid(leds, NUM_LEDS, CRGB::White); FastLED.show(); Serial.println(" " + String(i) + "..."); delay(500); // Turn off entire track FastLED.clear(); FastLED.show(); delay(300); } Serial.println(" GO! 🚀"); Serial.println(); } // ════════════════════════════════════════════════════════ // Get speed multiplier based on current position // ════════════════════════════════════════════════════════ float getSpeedMultiplier(int position) { // Hill 1 - Uphill (slow) or Downhill (fast) if(position >= HILL1_START && position <= HILL1_END) { if(position <= HILL1_PEAK) { return HILL1_UP_SPEED; // Climbing up - slow } else { return HILL1_DOWN_SPEED; // Going down - fast! } } // Hill 2 - Uphill (slow) or Downhill (fast) if(position >= HILL2_START && position <= HILL2_END) { if(position <= HILL2_PEAK) { return HILL2_UP_SPEED; // Climbing up - slow } else { return HILL2_DOWN_SPEED; // Going down - fast! } } // Horizontal Spiral - Fast! if(position >= SPIRAL1_START && position <= SPIRAL1_END) { return SPIRAL1_SPEED; } // Vertical Spiral - Very Fast! if(position >= SPIRAL2_START && position <= SPIRAL2_END) { return SPIRAL2_SPEED; } // Normal track - standard speed return 1.0; } // ════════════════════════════════════════════════════════ // Get zone name for current position // ════════════════════════════════════════════════════════ String getZoneName(int position) { if(position >= HILL1_START && position <= HILL1_END) { if(position <= HILL1_PEAK) { return "⛰️↗ HILL 1 UP"; } else { return "⛰️↘ HILL 1 DOWN"; } } if(position >= HILL2_START && position <= HILL2_END) { if(position <= HILL2_PEAK) { return "⛰️↗ HILL 2 UP"; } else { return "⛰️↘ HILL 2 DOWN"; } } if(position >= SPIRAL1_START && position <= SPIRAL1_END) { return "🌀 SPIRAL"; } if(position >= SPIRAL2_START && position <= SPIRAL2_END) { return "🌪️ V-SPIRAL"; } return ""; } // ════════════════════════════════════════════════════════ void updatePlayer(int playerIndex) { Player &p = players[playerIndex]; bool buttonCurrentlyPressed = (digitalRead(p.forwardPin) == LOW); // Button just pressed (transition from not pressed to pressed) if(buttonCurrentlyPressed && !p.buttonPressed) { p.buttonPressed = true; p.pressStartTime = millis(); p.moveExecuted = false; } // Button is being held down if(buttonCurrentlyPressed && p.buttonPressed && !p.moveExecuted) { unsigned long pressDuration = millis() - p.pressStartTime; // Long press detected (500ms+) → BOOST! if(pressDuration >= LONG_PRESS_TIME) { if(p.boostsRemaining > 0) { // Get speed multiplier for current position float speedMultiplier = getSpeedMultiplier(p.position); // Execute boost move with track speed modifier int actualMove = round(BOOSTED_SPEED * speedMultiplier); p.position += actualMove; p.boostsRemaining--; p.moveExecuted = true; // Cap at finish line if(p.position >= FINISH_LINE) { p.position = FINISH_LINE; playerFinished(playerIndex); } String zoneName = getZoneName(p.position); if(zoneName != "") { Serial.println("🚀 " + p.name + " BOOSTED +" + String(actualMove) + " to " + String(p.position) + " " + zoneName + " ⚡ Boosts: " + String(p.boostsRemaining)); } else { Serial.println("🚀 " + p.name + " BOOSTED to position " + String(p.position) + "! ⚡ Boosts left: " + String(p.boostsRemaining)); } } else { // No boosts left, do normal move instead float speedMultiplier = getSpeedMultiplier(p.position); int actualMove = round(NORMAL_SPEED * speedMultiplier); p.position += actualMove; p.moveExecuted = true; if(p.position >= FINISH_LINE) { p.position = FINISH_LINE; playerFinished(playerIndex); } Serial.println("→ " + p.name + " moved +" + String(actualMove) + " to " + String(p.position) + " ⚠️ NO BOOSTS LEFT!"); } } } // Button just released if(!buttonCurrentlyPressed && p.buttonPressed) { unsigned long pressDuration = millis() - p.pressStartTime; // Short press (less than 500ms) → Normal move with speed modifier if(pressDuration < LONG_PRESS_TIME && !p.moveExecuted) { float speedMultiplier = getSpeedMultiplier(p.position); int actualMove = round(NORMAL_SPEED * speedMultiplier); p.position += actualMove; // Cap at finish line if(p.position >= FINISH_LINE) { p.position = FINISH_LINE; playerFinished(playerIndex); } String zoneName = getZoneName(p.position); if(zoneName != "") { Serial.println("→ " + p.name + " moved +" + String(actualMove) + " to " + String(p.position) + " " + zoneName + " ⚡ Boosts: " + String(p.boostsRemaining)); } else { Serial.println("→ " + p.name + " moved to position " + String(p.position) + " ⚡ Boosts: " + String(p.boostsRemaining)); } } // Reset button state p.buttonPressed = false; p.moveExecuted = false; } } // ════════════════════════════════════════════════════════ void playerFinished(int playerIndex) { Player &p = players[playerIndex]; if(!p.finished) { p.finished = true; finishOrder++; p.finishTime = millis() - gameStartTime; Serial.println(); Serial.println("═══════════════════════════════════════════════════"); Serial.println(" 🏆 " + p.name + " FINISHED in position #" + String(finishOrder) + "!"); Serial.println(" Time: " + String(p.finishTime / 1000.0, 2) + " seconds"); Serial.println("═══════════════════════════════════════════════════"); Serial.println(); } } // ════════════════════════════════════════════════════════ void displayRace() { FastLED.clear(); // Draw finish line (white) leds[FINISH_LINE] = CRGB::White; // Draw track zone markers (subtle background colors) // Hill 1 Uphill - Dark red/brown (climbing is hard!) for(int i = HILL1_START; i <= HILL1_PEAK; i++) { leds[i] = CRGB(20, 10, 0); } // Hill 1 Downhill - Green tint (going fast!) for(int i = HILL1_PEAK + 1; i <= HILL1_END; i++) { leds[i] = CRGB(0, 20, 10); } // Hill 2 Uphill - Dark red/brown for(int i = HILL2_START; i <= HILL2_PEAK; i++) { leds[i] = CRGB(20, 10, 0); } // Hill 2 Downhill - Green tint for(int i = HILL2_PEAK + 1; i <= HILL2_END; i++) { leds[i] = CRGB(0, 20, 10); } // Horizontal Spiral - Cyan glow (fast zone!) for(int i = SPIRAL1_START; i <= SPIRAL1_END; i++) { leds[i] = CRGB(0, 15, 20); } // Vertical Spiral - Purple glow (super fast zone!) for(int i = SPIRAL2_START; i <= SPIRAL2_END; i++) { leds[i] = CRGB(20, 0, 20); } // Draw podium positions at finish line (for finished players) int finishPositions[3] = {FINISH_LINE, FINISH_LINE - 1, FINISH_LINE - 2}; // 299, 298, 297 int finishedPlayersSorted[3] = {-1, -1, -1}; int finishCount = 0; // Sort finished players by finish time for(int rank = 0; rank < 3; rank++) { unsigned long bestTime = 999999999; int bestPlayer = -1; for(int p = 0; p < 3; p++) { if(players[p].finished) { // Check if this player hasn't been placed yet bool alreadyPlaced = false; for(int r = 0; r < rank; r++) { if(finishedPlayersSorted[r] == p) { alreadyPlaced = true; break; } } if(!alreadyPlaced && players[p].finishTime < bestTime) { bestTime = players[p].finishTime; bestPlayer = p; } } } if(bestPlayer != -1) { finishedPlayersSorted[rank] = bestPlayer; finishCount++; } } // Display finished players on podium positions for(int rank = 0; rank < finishCount; rank++) { int playerIndex = finishedPlayersSorted[rank]; if(playerIndex != -1) { leds[finishPositions[rank]] = players[playerIndex].color; } } // Draw each player with a trail for(int i = 0; i < 3; i++) { Player &p = players[i]; if(!p.finished) { // Only draw trail and position for active players // Draw trail (fading) - OVERRIDE zone colors completely for(int t = 1; t <= TRAIL_LENGTH; t++) { int trailPos = p.position - t; if(trailPos >= 0 && trailPos < NUM_LEDS) { // Fade trail brightness but keep pure color CRGB trailColor = p.color; trailColor.fadeToBlackBy(t * 20); // OVERRIDE zone color completely (not blend) leds[trailPos] = trailColor; } } // Draw player position with visual feedback - PURE COLOR if(p.position < NUM_LEDS) { // Check if button is being held (charging boost) if(p.buttonPressed && !p.moveExecuted) { unsigned long pressDuration = millis() - p.pressStartTime; if(pressDuration < LONG_PRESS_TIME && p.boostsRemaining > 0) { // Charging animation - pulse brightness (pure color) uint8_t brightness = map(pressDuration, 0, LONG_PRESS_TIME, 50, 255); CRGB chargeColor = p.color; chargeColor.fadeToBlackBy(255 - brightness); leds[p.position] = chargeColor; } else if(pressDuration >= LONG_PRESS_TIME && p.boostsRemaining > 0) { // Fully charged - rapid white blink if(millis() % 100 < 50) { leds[p.position] = CRGB::White; } else { leds[p.position] = p.color; } } else if(p.boostsRemaining == 0) { // No boosts left - slow pulse (pure color) if(millis() % 500 < 250) { leds[p.position] = p.color; } else { leds[p.position] = CRGB::Black; } } } else { // Normal state - solid bright pure color (OVERRIDE zone) leds[p.position] = p.color; } } } } } // ════════════════════════════════════════════════════════ void showStartingPositions() { FastLED.clear(); // Show all players at starting line for(int i = 0; i < 3; i++) { leds[0] = leds[0] + players[i].color; // Blend all colors at start } // Show finish line leds[FINISH_LINE] = CRGB::White; FastLED.show(); } // ════════════════════════════════════════════════════════ void checkGameOver() { int finishedCount = 0; for(int i = 0; i < 3; i++) { if(players[i].finished) finishedCount++; } if(finishedCount >= 3) { gameOver = true; delay(1000); printFinalResults(); } } // ════════════════════════════════════════════════════════ void printFinalResults() { Serial.println(); Serial.println("╔═══════════════════════════════════════════════════╗"); Serial.println("║ 🏁 RACE COMPLETE! 🏁 ║"); Serial.println("╚═══════════════════════════════════════════════════╝"); Serial.println(); // Sort players by finish order (who finished first) Player sortedPlayers[3]; for(int i = 0; i < 3; i++) { sortedPlayers[i] = players[i]; } // Simple bubble sort by finish time for(int i = 0; i < 2; i++) { for(int j = 0; j < 2 - i; j++) { if(sortedPlayers[j].finishTime > sortedPlayers[j+1].finishTime) { Player temp = sortedPlayers[j]; sortedPlayers[j] = sortedPlayers[j+1]; sortedPlayers[j+1] = temp; } } } // Display podium Serial.println(" 🥇 1st Place: " + sortedPlayers[0].name + " - " + String(sortedPlayers[0].finishTime / 1000.0, 2) + "s"); Serial.println(" Boosts used: " + String(MAX_BOOSTS - sortedPlayers[0].boostsRemaining) + "/" + String(MAX_BOOSTS)); Serial.println(); Serial.println(" 🥈 2nd Place: " + sortedPlayers[1].name + " - " + String(sortedPlayers[1].finishTime / 1000.0, 2) + "s"); Serial.println(" Boosts used: " + String(MAX_BOOSTS - sortedPlayers[1].boostsRemaining) + "/" + String(MAX_BOOSTS)); Serial.println(); Serial.println(" 🥉 3rd Place: " + sortedPlayers[2].name + " - " + String(sortedPlayers[2].finishTime / 1000.0, 2) + "s"); Serial.println(" Boosts used: " + String(MAX_BOOSTS - sortedPlayers[2].boostsRemaining) + "/" + String(MAX_BOOSTS)); Serial.println(); Serial.println("──────────────────────────────────────────────────────"); Serial.println(" Press RESET to play again!"); Serial.println("══════════════════════════════════════════════════════"); } // ════════════════════════════════════════════════════════ void victoryAnimation() { static uint8_t hue = 0; // Rainbow wave celebration for(int i = 0; i < NUM_LEDS; i++) { leds[i] = CHSV(hue + (i * 255 / NUM_LEDS), 255, 255); } hue += 3; FastLED.show(); delay(20); }